//
//  GerritApi.swift
//  GerritHermes
//
//  Created by J.Zhou on 2020/1/6.
//  Copyright © 2020 J.Zhou. All rights reserved.
//

import Alamofire
import Combine

enum GerritError: Error, LocalizedError {
    case netError(String, Int?)
    case decodeError(String, Error)
    case rebaseConflict

    var errorDescription: String? {
        switch self {
        case .netError(let path, let code):
            return "NetError path: \(path), http code: \(code != nil ? "\(code!)" : "unknown")"
        case .decodeError(let path, let error):
            return "DecodeError path: \(path), error: \(error.localizedDescription)"
        case .rebaseConflict:
            return "RebaseConflict"
        }
    }
}

final class GerritApi {
    private let baseUrl: String
    private let user: String
    private let password: String
    private var requestManager: SessionManager = {
        let configuration = URLSessionConfiguration.default
        configuration.timeoutIntervalForRequest = 5
        configuration.timeoutIntervalForResource = 5
        return SessionManager(configuration: configuration)
    }()

    init(user: String, password: String, baseUrl: String) {
        self.user = user
        self.password = password
        self.baseUrl = baseUrl

        let plainString = "\(user):\(password)"
        let plainData = plainString.data(using: .utf8)
        let base64String = plainData?.base64EncodedString()

        requestManager.session.configuration.httpAdditionalHeaders = ["Authorization": "Basic " + base64String!]
    }

    private func prune(_ data: Data?) -> Data? {
        let magicPrefix = ")]}'\n"

        guard var data = data, data.count > magicPrefix.utf8.count else {
            return nil
        }
        data.removeFirst(magicPrefix.utf8.count)
        return data
    }

    private func get<T: Decodable>(_ path: String) -> AnyPublisher<T, GerritError> {
        return request(path, method: .get)
    }

    private func post<T: Decodable>(_ path: String, body: [String: Any]) -> AnyPublisher<T, GerritError> {
        return request(path, method: .post, parameters: body)
    }

    private func request<T: Decodable>(_ path: String, method: HTTPMethod, parameters: [String: Any]? = nil) -> AnyPublisher<T, GerritError> {
        let urlStr = baseUrl + path
        let user = self.user
        let password = self.password
        let plainString = "\(user):\(password)"
        let plainData = plainString.data(using: .utf8)
        let base64String = plainData?.base64EncodedString()

        let future: Future<Data?, GerritError> = Future { [weak self] (promise) in
            self?.requestManager.request(urlStr, method: method, parameters: parameters, encoding: JSONEncoding(), headers: ["Authorization": "Basic " + base64String!])
                .responseData { response in
                    switch response.result {
                    case .success(_):
                        promise(.success(response.data))
                    case .failure(_):
                        promise(.failure(.netError(path, response.response?.statusCode)))
                    }
                }
        }

        let decoder = JSONDecoder()
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
        formatter.timeZone = TimeZone(identifier: "GMT-8")
        decoder.dateDecodingStrategy = .formatted(formatter)

        return future.compactMap(prune)
            .decode(type: T.self, decoder: decoder)
            .mapError { GerritError.decodeError(path, $0) }
            .eraseToAnyPublisher()
    }
}

// MARK: - Fetch Info

extension GerritApi {
    func verifyAccount() -> AnyPublisher<AccountInfo, GerritError> {
        return get("/a/accounts/self")
    }

    func fetchChanges(project: String) -> AnyPublisher<[ChangeInfo], GerritError> {
        return get("/a/changes/?q=(status:open+project:\(project))")
    }

    func fetchChangeDetail(id: String) -> AnyPublisher<ChangeInfo, GerritError> {
        return get("/a/changes/\(id)/detail?o=CURRENT_REVISION&o=SUBMITTABLE")
    }

    func fetchChangeMessages(id: String) -> AnyPublisher<[ChangeMessageInfo], GerritError> {
        return get("/a/changes/\(id)/messages")
    }

    func fetchComments(id: String) -> AnyPublisher<[CommentInfo], GerritError> {
        let requestApi: AnyPublisher<[String: [CommentInfo]], GerritError> = get("/a/changes/\(id)/comments")
        return requestApi
            .map { (dict) -> [CommentInfo] in
                var result: [CommentInfo] = []
                for (path, comments) in dict {
                    let newComments = comments.map { (comment) -> CommentInfo in
                        var comment = comment
                        comment.path = path
                        return comment
                    }
                    result.append(contentsOf: newComments)
                }
                return result
            }
            .eraseToAnyPublisher()
    }
}

// MARK: - Modify

extension GerritApi {
    func rebaseChange(id: String, revisionId: String) -> AnyPublisher<ChangeInfo, GerritError> {
        return post("/a/changes/\(id)/revisions/\(revisionId)/rebase", body: [:])
    }

    func reviewPlus2(id: String, revisionId: String) -> AnyPublisher<ReviewResult, GerritError> {
        return post("/a/changes/\(id)/revisions/\(revisionId)/review",
            body: ["tag": "GerritHermes", "message": "Triggered by GerritHermes", "labels": ["Code-Review": +2], "comments": [:]])
    }

    func submit(id: String, revisionId: String) -> AnyPublisher<SubmitInfo, GerritError> {
        return post("/a/changes/\(id)/revisions/\(revisionId)/submit", body: [:])
    }

}
